直播转点播[合].js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501
  1. /**
  2. * 支持本地包直播链接
  3. * 传参 ?type=url&params=../json/live2cms.json
  4. live2cms.json
  5. 支持m3u类直播,支持线路归并。支持筛选切换显示模式
  6. [
  7. {
  8. "name": "GitHub",
  9. "url": "https://ghproxy.net/https://raw.githubusercontent.com/ssili126/tv/main/itvlist.txt"
  10. },
  11. {
  12. "name": "CNTV",
  13. "url": "./live_cntv.txt"
  14. }
  15. ]
  16. */
  17. /**
  18. * m3u直播格式转一般直播格式
  19. * @param m3u
  20. * @returns {string}
  21. */
  22. function convertM3uToNormal(m3u) {
  23. try {
  24. const lines = m3u.split('\n');
  25. let result = '';
  26. let TV = '';
  27. // let flag='#genre#';
  28. let flag = '#m3u#';
  29. let currentGroupTitle = '';
  30. lines.forEach((line) => {
  31. if (line.startsWith('#EXTINF:')) {
  32. line = line.replace(/'/g, '"');
  33. let groupTitle = '未知频道';
  34. let tvg_name = '';
  35. let tvg_logo = '';
  36. try {
  37. groupTitle = line.match(/group-title="(.*?)"/)[1].trim();
  38. } catch (e) {
  39. }
  40. try {
  41. tvg_name = line.match(/tvg-name="(.*?)"/)[1].trim();
  42. } catch (e) {
  43. }
  44. try {
  45. tvg_logo = line.match(/tvg-logo="(.*?)"/)[1].trim();
  46. } catch (e) {
  47. }
  48. TV = line.split(',').slice(-1)[0].trim();
  49. if (currentGroupTitle !== groupTitle) {
  50. currentGroupTitle = groupTitle;
  51. let ret_list = [currentGroupTitle, flag];
  52. // if(tvg_name){
  53. // ret_list.push(tvg_name);
  54. // }
  55. // if(tvg_logo){
  56. // ret_list.push(tvg_logo);
  57. // }
  58. result += `\n${ret_list.join(",")}\n`;
  59. }
  60. } else if (line.startsWith('http')) {
  61. const splitLine = line.split(',');
  62. result += `${TV}\,${splitLine[0]}\n`;
  63. }
  64. });
  65. // result = result.trim();
  66. result = mergeChannels(result);
  67. // log(result);
  68. return result
  69. } catch (e) {
  70. log(`m3u直播转普通直播发生错误:${e.message}`);
  71. return m3u
  72. }
  73. }
  74. /**
  75. * 线路归类/小棉袄算法
  76. * @param arr 数组
  77. * @param parse 解析式
  78. * @returns {[[*]]}
  79. */
  80. function splitArray(arr, parse) {
  81. parse = parse && typeof (parse) == 'function' ? parse : '';
  82. let result = [[arr[0]]];
  83. for (let i = 1; i < arr.length; i++) {
  84. let index = -1;
  85. for (let j = 0; j < result.length; j++) {
  86. if (parse && result[j].map(parse).includes(parse(arr[i]))) {
  87. index = j;
  88. } else if ((!parse) && result[j].includes(arr[i])) {
  89. index = j;
  90. }
  91. }
  92. if (index >= result.length - 1) {
  93. result.push([]);
  94. result[result.length - 1].push(arr[i]);
  95. } else {
  96. result[index + 1].push(arr[i]);
  97. }
  98. }
  99. return result;
  100. }
  101. /**
  102. * 搜索结果生成分组字典
  103. * @param arr
  104. * @param parse x=>x.split(',')[0]
  105. * @returns {{}}
  106. */
  107. function gen_group_dict(arr, parse) {
  108. let dict = {};
  109. arr.forEach((it) => {
  110. let k = it.split(',')[0];
  111. if (parse && typeof (parse) === 'function') {
  112. k = parse(k);
  113. }
  114. if (!dict[k]) {
  115. dict[k] = [it];
  116. } else {
  117. dict[k].push(it);
  118. }
  119. });
  120. return dict
  121. }
  122. /**
  123. * txt格式直播自动合并频道链接
  124. * @param text
  125. * @returns {string}
  126. */
  127. function mergeChannels(text) {
  128. const lines = text.split('\n');
  129. const channelMap = new Map();
  130. let currentChannel = ''; // 当前处理的频道
  131. lines.forEach(line => {
  132. // 使用正则表达式匹配频道行,假设频道行包含",#"即可识别为频道行
  133. if (/,#/.test(line)) {
  134. // 如果是频道名称,作为键值存储,初始化为空数组
  135. currentChannel = line;
  136. if (!channelMap.has(line)) {
  137. channelMap.set(line, []);
  138. }
  139. } else if (line) { // 忽略空行
  140. // 将当前行(链接)添加到当前频道数组中
  141. if (currentChannel) {
  142. channelMap.get(currentChannel).push(line);
  143. }
  144. }
  145. });
  146. // 构建结果字符串
  147. let result = '';
  148. channelMap.forEach((value, key) => {
  149. result += key + '\n' + value.join('\n') + '\n\n';
  150. });
  151. return result.trim(); // 移除尾部的多余换行符
  152. }
  153. globalThis.mergeChannels = mergeChannels;
  154. globalThis.convertM3uToNormal = convertM3uToNormal;
  155. globalThis.splitArray = splitArray;
  156. globalThis.gen_group_dict = gen_group_dict;
  157. globalThis.getRandomItem = function (items) {//从列表随机取出一个元素
  158. return items[Math.random() * items.length | 0];
  159. }
  160. globalThis.__ext = {data_dict: {}};
  161. var rule = {
  162. title: '直播转点播[合]',
  163. author: '道长',
  164. version: '20240628 beta7',
  165. update_info: `
  166. 20240628 beta6:
  167. 1.增加范冰冰v6源
  168. 2.修复带图标的m3u源识别
  169. 3.修复m3u8链接带参数转义问题
  170. 4.合并重复的频道名称下的链接
  171. 5.支持相对图片链接
  172. 20240627 beta1:
  173. 1.将原drpy项目的live2cms.js转换成hipy传参源。
  174. 【特别说明】支持m3u和txt的直播
  175. `,
  176. host: '',
  177. homeUrl: '',
  178. searchUrl: '#wd=**&pg=#TruePage##page=fypage',
  179. url: 'fyclass#pg=fypage&t=fyfilter',
  180. filter_url: '{{fl.show}}',
  181. headers: {'User-Agent': 'MOBILE_UA'},
  182. timeout: 5000, // class_name: '电影&电视剧&综艺&动漫',
  183. limit: 20,
  184. search_limit: 5, // 搜索限制取前5个,可以注释掉,就不限制搜索
  185. searchable: 1,//是否启用全局搜索,
  186. quickSearch: 0,//是否启用快速搜索,
  187. filterable: 1,//是否启用分类筛选,
  188. play_parse: true,
  189. // params: 'http://127.0.0.1:5707/files/json/live2cms.json',
  190. // 下面自定义一些源的配置
  191. // def_pic: 'https://avatars.githubusercontent.com/u/97389433?s=120&v=4', //默认列表图片
  192. def_pic: 'https://ghproxy.net/https://raw.githubusercontent.com/ls125781003/hikerjsonK/main/img/lives.jpg', //默认列表图片
  193. showMode: 'groups',// groups按组分类显示 all全部一条线路展示
  194. groupDict: {},// 搜索分组字典
  195. tips: '', //二级提示信息
  196. 预处理: $js.toString(() => {
  197. // 初始化保存的数据
  198. rule.showMode = getItem('showMode', 'groups');
  199. rule.groupDict = JSON.parse(getItem('groupDict', '{}'));
  200. rule.tips = `道长直播转点播js-当前版本${rule.version}`;
  201. if (typeof (batchFetch) === 'function') {
  202. // 支持批量请求直接放飞自我。搜索限制最大线程数量16
  203. rule.search_limit = 16;
  204. log('当前程序支持批量请求[batchFetch],搜索限制已设置为16');
  205. }
  206. let _url = rule.params;
  207. if (_url && typeof (_url) === 'string' && /^(http|file)/.test(_url)) {
  208. let html = request(_url);
  209. let json = JSON.parse(html);
  210. let _classes = [];
  211. rule.filter = {};
  212. rule.filter_def = {};
  213. json.forEach(it => {
  214. if (it.url && !/^(http|file)/.test(it.url)) {
  215. it.url = urljoin(_url, it.url);
  216. }
  217. if (it.img && !/^(http|file)/.test(it.img)) {
  218. it.img = urljoin(_url, it.img);
  219. }
  220. let _obj = {
  221. type_name: it.name,
  222. type_id: it.url,
  223. img: it.img,
  224. };
  225. _classes.push(_obj);
  226. let json1 = [{'n': '多线路分组', 'v': 'groups'}, {'n': '单线路', 'v': 'all'}];
  227. try {
  228. rule.filter[_obj.type_id] = [
  229. {'key': 'show', 'name': '播放展示', 'value': json1}
  230. ];
  231. if (json1.length > 0) {
  232. rule.filter_def[it.url] = {"show": json1[0].v};
  233. }
  234. } catch (e) {
  235. rule.filter[it.url] = json1
  236. }
  237. });
  238. __ext.data = json;
  239. rule.classes = _classes;
  240. }
  241. }),
  242. class_parse: $js.toString(() => {
  243. input = rule.classes;
  244. }),
  245. 推荐: $js.toString(() => {
  246. let update_info = [{
  247. vod_name: '更新日志',
  248. vod_id: 'update_info',
  249. vod_remarks: `版本:${rule.version}`,
  250. vod_pic: 'https://ghproxy.net/https://raw.githubusercontent.com/ls125781003/hikerjsonK/main/img/logo.png'
  251. }];
  252. VODS = [];
  253. if (rule.classes) {
  254. let randomClass = getRandomItem(rule.classes);
  255. let _get_url = randomClass.type_id;
  256. // let current_vod = rule.classes.find(item => item.type_id === _get_url);
  257. // let _pic = current_vod ? current_vod.img : '';
  258. let _pic = randomClass.img;
  259. let html;
  260. if (__ext.data_dict[_get_url]) {
  261. html = __ext.data_dict[_get_url];
  262. } else {
  263. html = request(_get_url);
  264. if (/#EXTM3U/.test(html)) {
  265. html = convertM3uToNormal(html);
  266. } else {
  267. html = mergeChannels(html);
  268. }
  269. __ext.data_dict[_get_url] = html;
  270. }
  271. let arr = html.match(/.*?[,,]#[\s\S].*?#/g); // 可能存在中文逗号
  272. try {
  273. arr.forEach(it => {
  274. let vname = it.split(/[,,]/)[0];
  275. let vtab = it.match(/#(.*?)#/)[0];
  276. VODS.push({
  277. vod_name: vname,
  278. vod_id: _get_url + '$' + vname,
  279. vod_pic: _pic || rule.def_pic,
  280. vod_remarks: vtab,
  281. });
  282. });
  283. } catch (e) {
  284. log(`直播转点播获取首页推荐发送错误:${e.message}`);
  285. }
  286. }
  287. VODS = update_info.concat(VODS);
  288. }),
  289. 一级: $js.toString(() => {
  290. VODS = [];
  291. // 一级限制页数不允许翻页
  292. if (rule.classes && MY_PAGE <= 1) {
  293. if (MY_FL.show) {
  294. rule.showMode = MY_FL.show;
  295. setItem('showMode', rule.showMode);
  296. }
  297. let _get_url = input.split('#')[0];
  298. let current_vod = rule.classes.find(item => item.type_id === MY_CATE);
  299. let _pic = current_vod ? current_vod.img : '';
  300. let html;
  301. if (__ext.data_dict[_get_url]) {
  302. html = __ext.data_dict[_get_url];
  303. } else {
  304. html = request(_get_url);
  305. if (/#EXTM3U/.test(html)) {
  306. html = convertM3uToNormal(html);
  307. } else {
  308. html = mergeChannels(html);
  309. }
  310. __ext.data_dict[_get_url] = html;
  311. }
  312. let arr = html.match(/.*?[,,]#[\s\S].*?#/g); // 可能存在中文逗号
  313. try {
  314. arr.forEach(it => {
  315. let vname = it.split(/[,,]/)[0];
  316. let vtab = it.match(/#(.*?)#/)[0];
  317. VODS.push({
  318. // vod_name:it.split(',')[0],
  319. vod_name: vname,
  320. vod_id: _get_url + '$' + vname,
  321. vod_pic: _pic || rule.def_pic,
  322. vod_remarks: vtab,
  323. });
  324. });
  325. } catch (e) {
  326. log(`直播转点播获取一级分类页发生错误:${e.message}`);
  327. }
  328. }
  329. }),
  330. 二级: $js.toString(() => {
  331. VOD = {};
  332. if (orId === 'update_info') {
  333. VOD = {
  334. vod_content: rule.update_info.trim(),
  335. vod_name: '更新日志',
  336. type_name: '更新日志',
  337. vod_pic: 'https://resource-cdn.tuxiaobei.com/video/FtWhs2mewX_7nEuE51_k6zvg6awl.png',
  338. vod_remarks: `版本:${rule.version}`,
  339. vod_play_from: '道长在线',
  340. // vod_play_url: '嗅探播放$https://resource-cdn.tuxiaobei.com/video/10/8f/108fc9d1ac3f69d29a738cdc097c9018.mp4',
  341. vod_play_url: '随机小视频$http://api.yujn.cn/api/zzxjj.php',
  342. };
  343. } else {
  344. if (rule.classes) {
  345. let _get_url = orId.split('$')[0];
  346. let _tab = orId.split('$')[1];
  347. if (orId.includes('#search#')) {
  348. let vod_name = _tab.replace('#search#', '');
  349. let vod_play_from = '来自搜索';
  350. vod_play_from += `:${_get_url}`;
  351. let vod_play_url = rule.groupDict[_get_url].map(x => x.replace(',', '$')).join('#');
  352. log(orId);
  353. VOD = {
  354. vod_name: '搜索:' + vod_name,
  355. type_name: "直播列表",
  356. vod_pic: rule.def_pic,
  357. // vod_content: orId,
  358. vod_content: orId.replace(getHome(orId), 'http://***'),
  359. vod_play_from: vod_play_from,
  360. vod_play_url: vod_play_url,
  361. vod_director: rule.tips,
  362. vod_remarks: rule.tips,
  363. }
  364. } else {
  365. let current_vod = rule.classes.find(item => item.type_id === _get_url);
  366. let _pic = current_vod ? current_vod.img : '';
  367. let html;
  368. if (__ext.data_dict[_get_url]) {
  369. html = __ext.data_dict[_get_url];
  370. } else {
  371. html = request(_get_url);
  372. if (/#EXTM3U/.test(html)) {
  373. html = convertM3uToNormal(html);
  374. } else {
  375. html = mergeChannels(html);
  376. }
  377. __ext.data_dict[_get_url] = html;
  378. }
  379. let a = new RegExp(`.*?${_tab.replace('(','\\(').replace(')','\\)')}[,,]#[\\s\\S].*?#`);
  380. let b = html.match(a)[0];
  381. let c = html.split(b)[1];
  382. if (c.match(/.*?[,,]#[\s\S].*?#/)) {
  383. let d = c.match(/.*?[,,]#[\s\S].*?#/)[0];
  384. c = c.split(d)[0];
  385. }
  386. let arr = c.trim().split('\n');
  387. let _list = [];
  388. arr.forEach((it) => {
  389. if (it.trim()) {
  390. let t = it.trim().split(',')[0];
  391. let u = it.trim().split(',')[1];
  392. _list.push(t + '$' + u);
  393. }
  394. });
  395. let vod_name = __ext.data.find(x => x.url === _get_url).name;
  396. let vod_play_url;
  397. let vod_play_from;
  398. if (rule.showMode === 'groups') {
  399. let groups = splitArray(_list, x => x.split('$')[0]);
  400. let tabs = [];
  401. for (let i = 0; i < groups.length; i++) {
  402. if (i === 0) {
  403. tabs.push(vod_name + '@1');
  404. } else {
  405. tabs.push(`@${i + 1}`);
  406. }
  407. }
  408. vod_play_url = groups.map(it => it.join('#')).join('$$$');
  409. vod_play_from = tabs.join('$$$');
  410. } else {
  411. vod_play_url = _list.join('#');
  412. vod_play_from = vod_name;
  413. }
  414. log(orId);
  415. VOD = {
  416. vod_id: orId,
  417. vod_name: vod_name + '|' + _tab,
  418. type_name: "直播列表",
  419. vod_pic: _pic || rule.def_pic,
  420. // vod_content: orId,
  421. vod_content: orId.replace(getHome(orId), 'http://***'),
  422. vod_play_from: vod_play_from,
  423. vod_play_url: vod_play_url,
  424. vod_director: rule.tips,
  425. vod_remarks: rule.tips,
  426. };
  427. }
  428. }
  429. }
  430. }),
  431. 搜索: $js.toString(() => {
  432. VODS = [];
  433. if (rule.classes && MY_PAGE <= 1) {
  434. let _get_url = __ext.data[0].url;
  435. let current_vod = rule.classes.find(item => item.type_id === _get_url);
  436. let _pic = current_vod ? current_vod.img : '';
  437. let html;
  438. if (__ext.data_dict[_get_url]) {
  439. html = __ext.data_dict[_get_url];
  440. } else {
  441. html = request(_get_url);
  442. if (/#EXTM3U/.test(html)) {
  443. html = convertM3uToNormal(html);
  444. } else {
  445. html = mergeChannels(html);
  446. }
  447. __ext.data_dict[_get_url] = html;
  448. }
  449. let str = '';
  450. Object.keys(__ext.data_dict).forEach(() => {
  451. str += __ext.data_dict[_get_url];
  452. });
  453. let links = str.split('\n').filter(it => it.trim() && it.includes(',') && it.split(',')[1].trim().startsWith('http'));
  454. links = links.map(it => it.trim());
  455. let plays = Array.from(new Set(links));
  456. log('搜索关键词:' + KEY);
  457. log('过滤前:' + plays.length);
  458. // plays = plays.filter(it => it.includes(KEY));
  459. plays = plays.filter(it => new RegExp(KEY, 'i').test(it));
  460. log('过滤后:' + plays.length);
  461. log(plays);
  462. let new_group = gen_group_dict(plays);
  463. rule.groupDict = Object.assign(rule.groupDict, new_group);
  464. // 搜索分组结果存至本地方便二级调用
  465. setItem('groupDict', JSON.stringify(rule.groupDict));
  466. // 返回的还是搜索的new_group
  467. Object.keys(new_group).forEach((it) => {
  468. VODS.push({
  469. 'vod_name': it,
  470. 'vod_id': it + '$' + KEY + '#search#',
  471. 'vod_pic': _pic || rule.def_pic,
  472. });
  473. });
  474. }
  475. }),
  476. lazy: $js.toString(() => {
  477. if (/\.(m3u8|mp4)/.test(input)) {
  478. if (input.includes('?') && typeof (playObj) == 'object' && playObj.url) {
  479. input = playObj.url;
  480. }
  481. input = {parse: 0, url: input}
  482. } else if (/yangshipin|1905\.com/.test(input)) {
  483. input = {parse: 1, jx: 0, url: input, js: '', header: {'User-Agent': PC_UA}, parse_extra: '&is_pc=1'};
  484. } else {
  485. input
  486. }
  487. }),
  488. }